# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule Ash.Filter.Runtime do
  @moduledoc """
  Tools to checks a record to see if it matches a filter statement, or to
  evaluate expressions against records.
  """

  alias Ash.Query.{BooleanExpression, Not, Ref}

  @doc """
  Removes any records that don't match the filter. Automatically loads
  if necessary. If there are any ambiguous terms in the filter (e.g things
  that could only be determined by data layer), it is assumed that they
  are not matches.
  """
  def filter_matches(domain, records, filter, opts \\ [])
  def filter_matches(_domain, [], _filter, _opts), do: {:ok, []}

  def filter_matches(_domain, records, nil, _opts), do: {:ok, records}

  def filter_matches(domain, records, filter, opts) do
    {records, parent} = load_records_and_parent(records, domain, opts[:parent], filter, opts)

    Enum.reduce_while(records, {:ok, []}, fn record, {:ok, records} ->
      case matches(record, filter, Keyword.put(opts, :parent, parent)) do
        {:ok, falsey} when falsey in [false, nil] ->
          {:cont, {:ok, records}}

        {:ok, _} ->
          {:cont, {:ok, [record | records]}}

        {:error, error} ->
          {:halt, {:error, error}}
      end
    end)
    |> case do
      {:ok, records} ->
        {:ok, Enum.reverse(records)}

      other ->
        other
    end
  end

  defp load_parent(nil, _, _, _), do: nil
  defp load_parent([], _, _, _), do: []

  defp load_parent([parent | rest], domain, filter, opts) do
    filter =
      filter
      |> Ash.Filter.flat_map(fn
        %Ash.Query.Parent{expr: expr} ->
          [expr]

        _ ->
          []
      end)

    {parent, rest} =
      load_records_and_parent(
        parent,
        domain,
        rest,
        filter,
        Keyword.put(opts, :parent_loaded?, true)
      )

    [parent | rest]
  end

  defp load_parent(value, domain, filter, opts), do: load_parent([value], domain, filter, opts)

  defp load_records_and_parent(records, domain, parent, filter, opts) do
    resource =
      case records do
        %resource{} ->
          resource

        [%resource{} | _] ->
          resource
      end

    refs_to_load =
      filter
      |> Ash.Filter.list_refs(false, false, true)
      |> Enum.map(&(&1.relationship_path ++ [&1.attribute]))
      |> Enum.reject(&Ash.Resource.loaded?(records, &1))
      |> Enum.map(&path_to_load(resource, &1))

    records =
      case refs_to_load do
        [] ->
          records

        refs_to_load ->
          load =
            resource
            |> load_all(refs_to_load)
            |> Ash.Query.set_context(%{private: %{internal?: true}})

          Ash.load!(records, load,
            authorize?: false,
            domain: domain,
            tenant: opts[:tenant],
            actor: opts[:actor]
          )
      end

    parent =
      if opts[:parent_loaded?] do
        parent
      else
        load_parent(parent, domain, filter, opts)
      end

    {records, parent}
  end

  defp matches(record, expression, opts) do
    relationship_paths =
      expression
      |> Ash.Filter.relationship_paths()

    record
    |> flatten_relationships(relationship_paths)
    |> Enum.reduce_while({:ok, false}, fn scenario, {:ok, false} ->
      case do_match(
             scenario,
             expression,
             opts[:parent],
             nil,
             opts[:unknown_on_unknown_refs?],
             opts[:conflicting_upsert_values]
           ) do
        {:error, error} ->
          {:halt, {:error, error}}

        :unknown ->
          {:halt, {:ok, false}}

        {:ok, val} when val not in [false, nil] ->
          {:halt, {:ok, true}}

        {:ok, _} ->
          {:cont, {:ok, false}}
      end
    end)
  end

  defp flatten_relationships(record, relationship_paths) do
    relationship_paths
    |> Enum.reject(&(&1 == []))
    |> Enum.reduce([record], fn [rel | rest], records ->
      Enum.flat_map(records, fn record ->
        case Map.get(record, rel) do
          nil ->
            [record]

          [] ->
            [record]

          %Ash.NotLoaded{} = not_loaded ->
            [Ash.Resource.set_metadata(record, %{unflattened_rels: %{rel => not_loaded}})]

          %Ash.ForbiddenField{} = forbidden ->
            [Ash.Resource.set_metadata(record, %{unflattened_rels: %{rel => forbidden}})]

          value when is_list(value) ->
            flatten_many_to_many(record, rel, value, rest)

          value ->
            flatten_to_one(record, rel, value, rest)
        end
      end)
    end)
  end

  defp flatten_many_to_many(record, rel, values, rest) do
    full_flattened = Enum.map(values, &flatten_relationships(&1, [rest])) |> List.flatten()

    Enum.map(full_flattened, fn value ->
      record
      |> Map.put(rel, value)
      |> Ash.Resource.set_metadata(%{unflattened_rels: %{rel => full_flattened}})
    end)
  end

  defp flatten_to_one(record, rel, value, rest) do
    full_flattened = flatten_relationships(value, [rest])

    Enum.map(full_flattened, fn value ->
      record
      |> Map.put(rel, value)
      |> Ash.Resource.set_metadata(%{unflattened_rels: %{rel => full_flattened}})
    end)
  end

  @doc false
  def load_and_eval(
        record,
        expression,
        parent \\ nil,
        resource \\ nil,
        domain \\ nil,
        unknown_on_unknown_refs? \\ false,
        actor \\ nil,
        tenant \\ nil
      ) do
    if domain && record do
      refs_to_load =
        expression
        |> Ash.Filter.list_refs(false, false, true)
        |> Enum.map(&(&1.relationship_path ++ [&1.attribute]))
        |> Enum.reject(&Ash.Resource.loaded?(record, &1))
        |> Enum.map(&path_to_load(resource, &1))

      record =
        case refs_to_load do
          [] ->
            record

          refs ->
            load =
              resource
              |> load_all(refs)
              |> Ash.Query.set_context(%{private: %{internal?: true}})

            Ash.load!(record, load,
              domain: domain,
              authorize?: false,
              tenant: tenant,
              actor: actor
            )
        end

      do_match(record, expression, parent, resource, unknown_on_unknown_refs?)
    else
      do_match(record, expression, parent, resource, unknown_on_unknown_refs?)
    end
  end

  @doc false
  def do_match(
        record,
        expression,
        parent \\ nil,
        resource \\ nil,
        unknown_on_unknown_refs? \\ false,
        conflicting_upsert_values \\ nil
      )

  def do_match(
        record,
        %Ash.Filter.Simple{predicates: predicates},
        parent,
        resource,
        unknown_on_unknown_refs?,
        conflicting_upsert_values
      ) do
    {:ok,
     Enum.all?(predicates, fn predicate ->
       do_match(
         record,
         predicate,
         parent,
         resource,
         unknown_on_unknown_refs?,
         conflicting_upsert_values
       ) == {:ok, true}
     end)}
  end

  def do_match(
        record,
        expression,
        parent,
        resource,
        unknown_on_unknown_refs?,
        conflicting_upsert_values
      ) do
    hydrated =
      case record do
        %resource{} ->
          Ash.Filter.hydrate_refs(expression, %{
            resource: resource,
            public?: false,
            parent_stack: parent_stack(parent),
            conflicting_upsert_values: conflicting_upsert_values
          })

        _ ->
          if resource do
            Ash.Filter.hydrate_refs(expression, %{
              resource: resource,
              public?: false,
              parent_stack: parent_stack(parent),
              conflicting_upsert_values: conflicting_upsert_values
            })
          else
            {:ok, expression}
          end
      end

    with {:ok, hydrated} <- hydrated do
      case resolve_expr(hydrated, record, parent, resource, unknown_on_unknown_refs?) do
        :unknown ->
          if unknown_on_unknown_refs? do
            :unknown
          else
            {:ok, nil}
          end

        {:ok, value} ->
          {:ok, value}

        other ->
          other
      end
    end
  end

  defp load_all(query, paths) do
    Enum.reduce(paths, query, &Ash.Query.load(&2, &1))
  end

  defp load_unflattened(record, []), do: record
  defp load_unflattened(nil, _), do: nil

  defp load_unflattened(%Ash.NotLoaded{}, _), do: nil

  defp load_unflattened(records, path) when is_list(records) do
    Enum.map(records, &load_unflattened(&1, path))
  end

  defp load_unflattened(record, [rel | rest]) do
    Map.put(
      record,
      rel,
      load_unflattened(record.__metadata__[:unflattened_rels][rel], rest)
    )
  end

  defp resolve_exprs(exprs, record, parent, resource, unknown_on_unknown_refs?) do
    exprs
    |> Enum.reduce_while({:ok, []}, fn expr, {:ok, exprs} ->
      case resolve_expr(expr, record, parent, resource, unknown_on_unknown_refs?) do
        {:ok, resolved} ->
          {:cont, {:ok, [resolved | exprs]}}

        {:error, error} ->
          {:halt, {:error, error}}

        :unknown ->
          {:halt, :unknown}
      end
    end)
    |> case do
      :unknown -> :unknown
      {:ok, resolved} -> {:ok, Enum.reverse(resolved)}
      {:error, error} -> {:error, error}
    end
  end

  defp resolve_expr({:_actor, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_arg, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_ref, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_ref, _, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_combinations, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_parent, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_parent, _, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_atomic_ref, _}, _, _, _, _), do: :unknown
  defp resolve_expr({:_context, _}, _, _, _, _), do: :unknown

  defp resolve_expr(
         %Ash.Filter{expression: expression},
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    resolve_expr(expression, record, parent, resource, unknown_on_unknown_refs?)
  end

  defp resolve_expr({key, value}, record, parent, resource, unknown_on_unknown_refs?)
       when is_atom(key) do
    case resolve_expr(value, record, parent, resource, unknown_on_unknown_refs?) do
      {:ok, resolved} ->
        {:ok, {key, resolved}}

      other ->
        other
    end
  end

  defp resolve_expr(%Ref{} = ref, record, parent, resource, unknown_on_unknown_refs?) do
    resolve_ref(ref, record, parent, resource, unknown_on_unknown_refs?)
  end

  defp resolve_expr(
         %BooleanExpression{op: :and, left: left, right: right},
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    with {:ok, left_resolved} when left_resolved not in [nil, false] <-
           resolve_expr(left, record, parent, resource, unknown_on_unknown_refs?),
         {:ok, right_resolved} <-
           resolve_expr(right, record, parent, resource, unknown_on_unknown_refs?) do
      cond do
        is_nil(left_resolved) ->
          {:ok, nil}

        is_nil(right_resolved) ->
          {:ok, nil}

        true ->
          {:ok, !!left_resolved and !!right_resolved}
      end
    end
  end

  defp resolve_expr(
         %BooleanExpression{op: :or, left: left, right: right},
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    with {:ok, left_resolved} when left_resolved in [nil, false] <-
           resolve_expr(left, record, parent, resource, unknown_on_unknown_refs?),
         {:ok, right_resolved} <-
           resolve_expr(right, record, parent, resource, unknown_on_unknown_refs?) do
      cond do
        left_resolved ->
          {:ok, !!left_resolved}

        is_nil(right_resolved) ->
          {:ok, right_resolved}

        true ->
          {:ok, !!right_resolved}
      end
    end
  end

  defp resolve_expr(
         %Not{expression: expression},
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    case resolve_expr(expression, record, parent, resource, unknown_on_unknown_refs?) do
      {:ok, nil} -> {:ok, nil}
      {:ok, resolved} -> {:ok, !resolved}
      other -> other
    end
  end

  defp resolve_expr(
         %Ash.CustomExpression{simple_expression: {:ok, expression}},
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    case resolve_expr(expression, record, parent, resource, unknown_on_unknown_refs?) do
      {:ok, resolved} -> {:ok, resolved}
      other -> other
    end
  end

  defp resolve_expr(
         %Ash.CustomExpression{expression: expression},
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    case resolve_expr(expression, record, parent, resource, unknown_on_unknown_refs?) do
      {:ok, resolved} -> {:ok, resolved}
      other -> other
    end
  end

  defp resolve_expr(
         %Ash.Query.Parent{},
         _,
         parent,
         _resource,
         unknown_on_unknown_refs?
       )
       when is_nil(parent) or parent == [] do
    if unknown_on_unknown_refs? do
      :unknown
    else
      {:ok, nil}
    end
  end

  defp resolve_expr(
         %Ash.Query.Parent{} = parent_expr,
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       )
       when not is_list(parent) do
    resolve_expr(parent_expr, record, [parent], resource, unknown_on_unknown_refs?)
  end

  defp resolve_expr(
         %Ash.Query.Parent{expr: expr},
         _,
         [parent | rest],
         resource,
         unknown_on_unknown_refs?
       ) do
    resolve_expr(expr, parent, rest, resource, unknown_on_unknown_refs?)
  end

  defp resolve_expr(
         %Ash.Query.UpsertConflict{} = expr,
         _record,
         _parent,
         _resource,
         _unknown_on_unknown_refs?
       ) do
    {:error, "#{inspect(expr)} not implemented for data source"}
  end

  defp resolve_expr(%Ash.Query.Exists{}, nil, _parent, _resource, unknown_on_unknown_refs?) do
    if unknown_on_unknown_refs? do
      :unknown
    else
      {:ok, nil}
    end
  end

  defp resolve_expr(
         %Ash.Query.Exists{
           at_path: [],
           path: path,
           expr: expr,
           related?: related?,
           resource: unrelated_resource
         },
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    if related? do
      record
      |> flatten_relationships([path])
      |> load_unflattened(path)
      |> get_related(path, unknown_on_unknown_refs?)
      |> case do
        :unknown ->
          :unknown

        related ->
          related
          |> List.wrap()
          |> Enum.reduce_while({:ok, false}, fn related, {:ok, false} ->
            case resolve_expr(
                   expr,
                   related,
                   [record | List.wrap(parent)],
                   resource,
                   unknown_on_unknown_refs?
                 ) do
              {:ok, falsy} when falsy in [nil, false] ->
                {:cont, {:ok, false}}

              {:ok, _} ->
                {:halt, {:ok, true}}

              other ->
                {:halt, other}
            end
          end)
      end
    else
      resolve_unrelated_exists(
        unrelated_resource,
        expr,
        record,
        parent,
        unknown_on_unknown_refs?
      )
    end
  end

  defp resolve_expr(
         %Ash.Query.Exists{at_path: at_path} = exists,
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    record
    |> flatten_relationships([at_path])
    |> get_related(at_path, unknown_on_unknown_refs?)
    |> case do
      :unknown ->
        :unknown

      related ->
        related
        |> Enum.reduce_while({:ok, false}, fn related, {:ok, false} ->
          case resolve_expr(
                 %{exists | at_path: []},
                 related,
                 parent,
                 resource,
                 unknown_on_unknown_refs?
               ) do
            {:ok, true} ->
              {:halt, {:ok, true}}

            {:ok, _} ->
              {:cont, {:ok, false}}

            :unknown ->
              {:halt, :unknown}

            other ->
              {:halt, other}
          end
        end)
    end
  end

  defp resolve_expr(
         %mod{__predicate__?: _, left: left, right: right} = pred,
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    case partial_evaluate(
           pred,
           record,
           parent,
           resource,
           unknown_on_unknown_refs?
         ) do
      {:ok, ^pred} ->
        with {:ok, [left, right]} <-
               resolve_exprs([left, right], record, parent, resource, unknown_on_unknown_refs?),
             {:op, {:ok, new_pred}} <-
               {:op, Ash.Query.Operator.try_cast_with_ref(mod, left, right)},
             {:known, val} <-
               evaluate(new_pred, record, parent, resource, unknown_on_unknown_refs?) do
          {:ok, val}
        else
          {:op, {:error, error}} ->
            {:error, error}

          {:op, {:ok, expr}} ->
            resolve_expr(expr, record, parent, resource, unknown_on_unknown_refs?)

          {:op, {:known, value}} ->
            {:ok, value}

          {:error, error} ->
            {:error, error}

          {:op, :unknown} ->
            :unknown

          :unknown ->
            :unknown

          _ ->
            {:ok, nil}
        end

      {:ok, other} ->
        resolve_expr(other, record, parent, resource, unknown_on_unknown_refs?)

      {:error, error} ->
        {:error, error}
    end
  end

  defp resolve_expr(
         %mod{__predicate__?: _, arguments: args} = pred,
         record,
         parent,
         resource,
         unknown_on_unknown_refs?
       ) do
    case partial_evaluate(
           pred,
           record,
           parent,
           resource,
           unknown_on_unknown_refs?
         ) do
      {:ok, ^pred} ->
        # resolve arguments one at a time from left to right, and attempt to partial evaluate again.
        # required for short circuiting `and`, `or`, `||`, `&&` and `if`. Its kind of a hacky
        # short circuiting but no functions that I know of need different behavior,
        # and if they do we can add a new callback or something.
        case Enum.find_index(args, &Ash.Expr.expr?/1) do
          index when is_integer(index) ->
            case resolve_expr(
                   Enum.at(args, index),
                   record,
                   parent,
                   resource,
                   unknown_on_unknown_refs?
                 ) do
              {:ok, new_value} ->
                resolve_expr(
                  %{
                    pred
                    | arguments: List.replace_at(args, index, new_value)
                  },
                  record,
                  parent,
                  resource,
                  unknown_on_unknown_refs?
                )

              {:error, error} ->
                {:error, error}

              :unknown ->
                :unknown
            end

          nil ->
            with {:ok, args} <-
                   resolve_exprs(args, record, parent, resource, unknown_on_unknown_refs?),
                 {:args, args} when not is_nil(args) <-
                   {:args, try_cast_arguments(mod.args(), args)},
                 {:known, val} <-
                   evaluate(
                     %{pred | arguments: args},
                     record,
                     parent,
                     resource,
                     unknown_on_unknown_refs?
                   ) do
              {:ok, val}
            else
              {:args, nil} ->
                {:error,
                 "Could not cast function arguments for #{mod.name()}/#{Enum.count(args)}"}

              {:error, error} ->
                {:error, error}

              :unknown ->
                :unknown

              _ ->
                {:ok, nil}
            end
        end

      {:ok, other} ->
        resolve_expr(other, record, parent, resource, unknown_on_unknown_refs?)

      {:error, error} ->
        {:error, error}
    end
  end

  defp resolve_expr(list, record, parent, resource, unknown_on_unknown_refs?)
       when is_list(list) do
    list
    |> Enum.reduce_while({:ok, []}, fn item, {:ok, acc} ->
      case resolve_expr(item, record, parent, resource, unknown_on_unknown_refs?) do
        {:ok, result} ->
          {:cont, {:ok, [result | acc]}}

        :unknown ->
          {:halt, :unknown}

        {:error, error} ->
          {:halt, {:error, error}}
      end
    end)
    |> case do
      {:ok, results} -> {:ok, Enum.reverse(results)}
      other -> other
    end
  end

  defp resolve_expr(map, record, parent, resource, unknown_on_unknown_refs?)
       when is_map(map) and not is_struct(map) do
    Enum.reduce_while(map, {:ok, %{}}, fn {key, value}, {:ok, acc} ->
      with {:ok, key} <- resolve_expr(key, record, parent, resource, unknown_on_unknown_refs?),
           {:ok, value} <- resolve_expr(value, record, parent, resource, unknown_on_unknown_refs?) do
        {:cont, {:ok, Map.put(acc, key, value)}}
      else
        other ->
          {:halt, other}
      end
    end)
  end

  defp resolve_expr(other, _, _, _, _), do: {:ok, other}

  defp try_cast_arguments(:var_args, args) do
    args
    |> Enum.map(fn _ -> :any end)
    |> Ash.Query.Function.try_cast_arguments(args)
  end

  defp try_cast_arguments(configured_args, args) do
    given_arg_count = Enum.count(args)

    configured_args
    |> Enum.filter(fn args ->
      Enum.count(args) == given_arg_count
    end)
    |> Enum.find_value(&Ash.Query.Function.try_cast_arguments(&1, args))
  end

  defp resolve_ref(
         %Ash.Query.Ref{
           relationship_path: relationship_path,
           resource: resource,
           combinations?: combinations?,
           attribute: %Ash.Query.Calculation{
             module: module,
             opts: opts,
             name: name,
             load: load,
             context: context
           }
         },
         record,
         parent,
         _resource,
         unknown_on_unknown_refs?
       )
       when combinations? != true do
    result =
      record
      |> get_related(relationship_path, unknown_on_unknown_refs?)
      |> case do
        :unknown ->
          if unknown_on_unknown_refs? do
            :unknown
          else
            {:ok, nil}
          end

        [] ->
          {:ok, nil}

        [record | _] ->
          if load do
            case Map.fetch(record || %{}, load) do
              :error ->
                :unknown

              {:ok, %Ash.NotLoaded{}} ->
                :unknown

              {:ok, %Ash.ForbiddenField{}} ->
                :unknown

              {:ok, other} ->
                {:ok, other}
            end
          else
            case Map.fetch(Map.get(record || %{}, :calculations, %{}), name) do
              {:ok, value} ->
                {:ok, value}

              :error ->
                :unknown
            end
          end
      end

    case result do
      {:ok, result} ->
        {:ok, result}

      :unknown ->
        if module.has_expression?() do
          expression = module.expression(opts, context)

          expression =
            Ash.Expr.fill_template(
              expression,
              actor: context.actor,
              tenant: context.tenant,
              args: context.arguments,
              context: context.source_context
            )

          hydrated =
            Ash.Filter.hydrate_refs(expression, %{
              resource: resource,
              public?: false,
              parent_stack: parent_stack(parent)
            })

          with {:ok, hydrated} <- hydrated do
            hydrated
            |> Ash.Filter.prefix_refs(relationship_path)
            |> resolve_expr(record, parent, resource, unknown_on_unknown_refs?)
          end
        else
          if unknown_on_unknown_refs? do
            :unknown
          else
            # We need to rewrite this
            # As it stands now, it will evaluate the calculation
            # once per expanded result. I'm not sure what that will
            # look like though.

            if record do
              case module.calculate([record], opts, context) do
                [result] ->
                  {:ok, result}

                {:ok, [result]} ->
                  {:ok, result}

                _ ->
                  {:ok, nil}
              end
            else
              if unknown_on_unknown_refs? do
                :unknown
              else
                {:ok, nil}
              end
            end
          end
        end
    end
  end

  defp resolve_ref(_ref, nil, _, _resource, unknown_on_unknown_refs?) do
    if unknown_on_unknown_refs? do
      :unknown
    else
      {:ok, nil}
    end
  end

  defp resolve_ref(%Ash.Query.Ref{combinations?: true, attribute: %{load?: false}}, _, _, _, true) do
    :unknown
  end

  defp resolve_ref(
         %Ash.Query.Ref{attribute: %{name: name}, combinations?: true},
         record,
         _,
         _,
         unknown_on_unknown_refs?
       ) do
    with :error <- Map.fetch(record.calculations, name),
         :error <- Map.fetch(record, name) do
      if unknown_on_unknown_refs? do
        :unknown
      else
        {:ok, nil}
      end
    else
      {:ok, value} -> {:ok, value}
    end
  end

  defp resolve_ref(
         %Ash.Query.Ref{
           relationship_path: [],
           attribute: %Ash.Query.Aggregate{
             load: load,
             name: name
           }
         },
         record,
         _parent,
         _resource,
         unknown_on_unknown_refs?
       ) do
    if load do
      case Map.get(record, load) do
        %Ash.ForbiddenField{} ->
          if unknown_on_unknown_refs? do
            :unknown
          else
            {:ok, nil}
          end

        %Ash.NotLoaded{} ->
          if unknown_on_unknown_refs? do
            :unknown
          else
            {:ok, nil}
          end

        other ->
          {:ok, other}
      end
    else
      case Map.fetch(record.aggregates, name) do
        {:ok, value} ->
          {:ok, value}

        :error ->
          if unknown_on_unknown_refs? do
            :unknown
          else
            {:ok, nil}
          end
      end
    end
  end

  defp resolve_ref(
         %Ref{attribute: attribute, relationship_path: path},
         record,
         _parent,
         _resource,
         unknown_on_unknown_refs?
       ) do
    name =
      case attribute do
        %{name: name} -> name
        name -> name
      end

    record
    |> get_related(path, unknown_on_unknown_refs?)
    |> case do
      :unknown ->
        if unknown_on_unknown_refs? do
          :unknown
        else
          {:ok, nil}
        end

      [] ->
        {:ok, nil}

      [%struct{} = record | _] ->
        if Spark.Dsl.is?(struct, Ash.Resource) do
          if Ash.Resource.Info.attribute(struct, name) do
            if Ash.Resource.selected?(record, name) do
              {:ok, Map.get(record, name)}
            else
              if unknown_on_unknown_refs? do
                :unknown
              else
                {:ok, nil}
              end
            end
          else
            if Ash.Resource.loaded?(record, name) do
              {:ok, Map.get(record, name)}
            else
              if unknown_on_unknown_refs? do
                :unknown
              else
                {:ok, nil}
              end
            end
          end
        else
          {:ok, Map.get(record, name)}
        end

      [record | _] ->
        {:ok, Map.get(record, name)}

      _ ->
        {:ok, nil}
    end
    |> or_default(attribute)
  end

  defp resolve_ref(_value, _record, _, _, true), do: :unknown
  defp resolve_ref(_value, _record, _, _, _), do: {:ok, nil}

  defp or_default({:ok, nil}, %Ash.Resource.Aggregate{default: default})
       when not is_nil(default) do
    if is_function(default) do
      {:ok, default.()}
    else
      {:ok, default}
    end
  end

  defp or_default({:ok, nil}, %Ash.Query.Aggregate{default_value: default})
       when not is_nil(default) do
    if is_function(default) do
      {:ok, default.()}
    else
      {:ok, default}
    end
  end

  defp or_default(other, _), do: other

  defp path_to_load(_resource, [%struct{name: name}])
       when struct in [
              Ash.Resource.Attribute,
              Ash.Resource.Aggregate,
              Ash.Resource.Calculation
            ] do
    [name]
  end

  defp path_to_load(_, []), do: []

  defp path_to_load(_resource, [other]) do
    [other]
  end

  defp path_to_load(resource, [first | rest]) do
    [{first, path_to_load(resource, rest)}]
  end

  @doc false
  def get_related(
        source,
        path,
        unknown_on_unknown_refs?,
        join_filters \\ %{},
        parent_stack \\ [],
        domain \\ nil
      )

  def get_related(nil, _, unknown_on_unknown_refs?, _join_filters, _parent_stack, _domain) do
    if unknown_on_unknown_refs? do
      :unknown
    else
      []
    end
  end

  def get_related(
        %Ash.ForbiddenField{},
        _,
        unknown_on_unknown_refs?,
        _join_filters,
        _parent_stack,
        _domain
      ) do
    if unknown_on_unknown_refs? do
      :unknown
    else
      []
    end
  end

  def get_related(
        %Ash.NotLoaded{},
        _,
        unknown_on_unknown_refs?,
        _join_filters,
        _parent_stack,
        _domain
      ) do
    if unknown_on_unknown_refs? do
      :unknown
    else
      []
    end
  end

  def get_related(record, [], _, _, _parent_stack, _domain) do
    List.wrap(record)
  end

  def get_related(
        records,
        [key | rest],
        unknown_on_unknown_refs?,
        join_filters,
        parent_stack,
        domain
      )
      when is_list(records) do
    {join_filter, rest_join_filters} = Map.pop(join_filters, [])

    rest_join_filters =
      Enum.reduce(rest_join_filters, %{}, fn {path, filter}, acc ->
        if List.starts_with?(path, [key]) do
          Map.put(acc, Enum.drop(path, 1), filter)
        else
          acc
        end
      end)

    filtered =
      if Map.has_key?(join_filters, []) do
        filter_matches(domain, records, join_filter,
          parent: parent_stack,
          unknown_on_unknown_refs?: unknown_on_unknown_refs?
        )
      else
        {:ok, records}
      end

    case filtered do
      {:ok, matches} ->
        matches
        |> Enum.reduce_while([], fn match, acc ->
          case Map.get(match, key) do
            %Ash.NotLoaded{} when unknown_on_unknown_refs? ->
              {:halt, :unknown}

            %Ash.ForbiddenField{} when unknown_on_unknown_refs? ->
              {:halt, :unknown}

            nil when unknown_on_unknown_refs? ->
              {:halt, :unknown}

            nil ->
              {:cont, acc}

            match_keys ->
              match_keys
              |> List.wrap()
              |> Enum.reduce_while([], fn this_match, inner_acc ->
                case get_related(
                       this_match,
                       rest,
                       unknown_on_unknown_refs?,
                       rest_join_filters,
                       [match | parent_stack],
                       domain
                     ) do
                  :unknown -> {:halt, :unknown}
                  value -> {:cont, inner_acc ++ List.wrap(value)}
                end
              end)
              |> case do
                :unknown -> {:halt, :unknown}
                list -> {:cont, acc ++ list}
              end
          end
        end)

      _ ->
        if unknown_on_unknown_refs? do
          :unknown
        else
          []
        end
    end
  end

  def get_related(
        record,
        [key | _] = path,
        unknown_on_unknown_refs?,
        join_filters,
        parent_stack,
        domain
      ) do
    case Map.get(record, key) do
      %Ash.NotLoaded{} when unknown_on_unknown_refs? ->
        :unknown

      %Ash.ForbiddenField{} when unknown_on_unknown_refs? ->
        :unknown

      nil when unknown_on_unknown_refs? ->
        :unknown

      _value ->
        case get_related(
               [record],
               path,
               unknown_on_unknown_refs?,
               join_filters,
               parent_stack,
               domain
             ) do
          :unknown ->
            if unknown_on_unknown_refs? do
              :unknown
            else
              []
            end

          related ->
            List.wrap(related)
        end
    end
  end

  def old_get_related(
        records,
        [key | rest],
        unknown_on_unknown_refs?,
        join_filters,
        parent_stack,
        domain
      )
      when is_list(records) do
    {join_filter, rest_join_filters} = Map.pop(join_filters, [])

    rest_join_filters =
      Enum.reduce(rest_join_filters, %{}, fn {path, filter}, acc ->
        if List.starts_with?(path, [key]) do
          Map.put(acc, Enum.drop(path, 1), filter)
        else
          acc
        end
      end)

    filtered =
      if Map.has_key?(join_filters, []) do
        filter_matches(domain, records, join_filter,
          parent: parent_stack,
          unknown_on_unknown_refs?: unknown_on_unknown_refs?
        )
      else
        {:ok, records}
      end

    case filtered do
      {:ok, matches} ->
        matches
        |> Enum.flat_map(fn match ->
          case Map.get(match, key) do
            :unknown ->
              []

            nil ->
              []

            matches ->
              matches
              |> List.wrap()
              |> Enum.flat_map(&List.wrap/1)
              |> Enum.reject(&(&1 in [:unknown, nil]))
              |> Enum.flat_map(fn this_match ->
                case get_related(
                       this_match,
                       rest,
                       unknown_on_unknown_refs?,
                       rest_join_filters,
                       [match | parent_stack],
                       domain
                     ) do
                  :unknown -> []
                  value -> value
                end
              end)
              |> List.flatten()
          end
        end)

      _ ->
        if unknown_on_unknown_refs? do
          :unknown
        else
          []
        end
    end
  end

  defp parent_stack(parent) do
    parent
    |> List.wrap()
    |> Enum.map(fn
      %resource{} ->
        resource

      resource ->
        resource
    end)
  end

  defp evaluate(
         %{__function__?: true} = func,
         _record,
         _parent,
         _resource,
         _unknown_on_unknown_refs?
       ),
       do: Ash.Query.Function.evaluate(func)

  defp evaluate(
         %{__operator__?: true} = op,
         _record,
         _parent,
         _resource,
         _unknown_on_unknown_refs?
       ),
       do: Ash.Query.Operator.evaluate(op)

  defp evaluate(other, record, parent, resource, unknown_on_unknown_refs?) do
    case resolve_expr(other, record, parent, resource, unknown_on_unknown_refs?) do
      {:ok, value} -> {:known, value}
      other -> other
    end
  end

  defp partial_evaluate(
         %mod{__function__?: _} = pred,
         _record,
         _parent,
         _resource,
         _unknown_on_unknown_refs?
       ) do
    if mod.has_partial_evaluate?() do
      mod.partial_evaluate(pred)
    else
      {:ok, pred}
    end
  end

  defp partial_evaluate(other, _record, _parent, _resource, _unknown_on_unknown_refs?) do
    {:ok, other}
  end

  defp resolve_unrelated_exists(
         unrelated_resource,
         expr,
         record,
         parent,
         unknown_on_unknown_refs?
       ) do
    resolved_expr = resolve_parent_expressions(expr, record, parent, unknown_on_unknown_refs?)

    query =
      unrelated_resource
      |> Ash.Query.new()
      |> Ash.Query.set_context(%{private: %{internal?: true}})
      |> Ash.Query.do_filter(resolved_expr)

    Ash.exists(query, authorize?: false)
  end

  defp resolve_parent_expressions(expr, record, parent, unknown_on_unknown_refs?) do
    Ash.Filter.map(expr, fn
      %Ash.Query.Parent{expr: parent_expr} ->
        case resolve_expr(parent_expr, record, List.wrap(parent), nil, unknown_on_unknown_refs?) do
          {:ok, value} -> value
          _ -> nil
        end

      other ->
        other
    end)
  end
end
